package dpss.node.server;

import java.awt.PageAttributes.OriginType;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import dpss.commons.CustomLogger;
import dpss.commons.ServerNode;
import dpss.commons.UDPMessage;

/**
 * Manages incoming requests using a FIFO queue
 * 
 * @author musab.mirza
 *
 */
public class FifoQueueProcessor extends Thread {
	
	private static CustomLogger logger = CustomLogger.getInstance();

	// A FIFO request Queue (used if this is a leader)
	// This will be managed by a separate thread (fifoQueueThread)
	private LinkedBlockingQueue<UDPMessage> fifoQueue;	// Shared resource
	private Processor processor;
	private DatagramSocket socket;
	private HashSet<InetSocketAddress> myReplicas;
	private final AtomicInteger waitingForReplicas;
	private boolean die = false;

	public FifoQueueProcessor(DatagramSocket socket, Processor processor, LinkedBlockingQueue<UDPMessage> fifoQueue, HashSet<InetSocketAddress> myReplicas) {
		this.processor = processor;
		this.fifoQueue = fifoQueue;
		this.socket = socket;
		this.myReplicas = myReplicas;
		this.waitingForReplicas = new AtomicInteger(0);
	}

	/**
	 * Clears the queue
	 */
	public void clear() {
		synchronized(fifoQueue){
			fifoQueue.clear();
		}
	}

	/**
	 * Adds a request to the queue
	 * @param request
	 */
	public void queue(UDPMessage request) {
		synchronized(fifoQueue){
			try {
				fifoQueue.put(request);
				printFifoQueue();
			} catch (InterruptedException e) {
				logger.log("ERROR - Unable to queue message: " + request.toString());
			}
		}
	}
	
	
	/**
	 * The main runner. Keeps checking if there is anything in the queue, and processes it
	 * using the processor
	 */
	public void run(){
		while(!die){
			
			
			synchronized (fifoQueue) {
				
				// If the queue has a request, pick it up
				if (!fifoQueue.isEmpty()){
					
					UDPMessage earliestRequest = fifoQueue.remove();
					InetSocketAddress earliestRequestSender = earliestRequest.getSenderSocketAddress();
					
					logger.log("Pulled request from FIFO Queue: " + earliestRequest.toString());

					// Send pings to all replicas

					waitingForReplicas.set(myReplicas.size());
					for(InetSocketAddress replicaAddress : myReplicas){
						logger.log("Forwarding the request to replica: " + replicaAddress.toString());
						UDPMessage.sendMessage(socket, new UDPMessage("dpss_replica_request", null, true, ""), replicaAddress);
					}
					logger.log("Waiting for all "+waitingForReplicas.get()+" replica(s) to respond...");

					

					// Wait for all replicas to finish and then process your request
					while(true){
						if (waitingForReplicas.get() == 0){
							logger.log("All replicas has reponded, now processing the main request");
							processRequest(earliestRequest, earliestRequestSender);
							break;
						}
					}
					

				}
			}
		}
	}
	
	

	/**
	 * Gets the hashmap from the UDPrequest
	 * @param request
	 * @return
	 */
	public static HashMap<String, String> extractDataHashMap(UDPMessage request) {
		return (HashMap<String, String>) request.getData();
	}

	/**
	 * Processes a given requests
	 * @param earliestRequestSender 
	 * @param earliestRequest
	 */
	private synchronized void processRequest(UDPMessage request, InetSocketAddress earliestRequestSender) {
		
		logger.log("Leader will process and send the request to:  is: " + earliestRequestSender.toString());
				
		
		// Extract the data first
		HashMap<String, String> data = extractDataHashMap(request);
		
		// Check what type of request it was and respond accordingly
		UDPMessage toSend = null;
		switch (request.getMessage()){
		
		case "dpss_playerSignIn":	
			try {
				HashMap<String, String> responseData = processor.playerSignIn(data.get("username"), data.get("password"), data.get("ipAddress"));
				toSend = new UDPMessage("dpss_playerSignIn_response", responseData, true, "");
			} catch (Exception e) {
				toSend =  new UDPMessage("dpss_playerSignIn_response", null, false, e.getMessage());
			}
			break;
			
			
		case "dpss_playerSignOut":
			try {
				processor.playerSignOut(data.get("username"), data.get("ipAddress"));
				toSend =  new UDPMessage("dpss_playerSignOut_response", null, true, "");
			} catch (Exception e) {
				toSend =  new UDPMessage("dpss_playerSignOut_response", null, false, e.getMessage());
			}
			break;
			
		case "dpss_createPlayerAccount":
			try {
				processor.createPlayerAccount(data.get("firstName"), data.get("lastName"), Short.parseShort(data.get("age")), data.get("username"), data.get("password"), data.get("ipAddress"));
				toSend = new UDPMessage("dpss_createPlayerAccount_response", null, true, "");
			} catch (Exception e) {
				toSend = new UDPMessage("dpss_createPlayerAccount_response", null, false, e.getMessage());
			}
			break;
			
		case "dpss_suspendAccount":
			try {
				processor.suspendAccount(data.get("adminUsername"), data.get("adminPassword"), data.get("ipAddress"), data.get("usernameToSuspend"));
				toSend =  new UDPMessage("dpss_suspendAccount_response", null, true, "");
			} catch (Exception e) {
				toSend = new UDPMessage("dpss_suspendAccount_response", null, false, e.getMessage());
			}
			break;
			
			
		case "dpss_transferAccount":
			try {
				processor.transferAccount(data.get("username"), data.get("password"), data.get("oldIpAddress"), data.get("newIpAddress"));
				toSend =  new UDPMessage("dpss_transferAccount_response", null, true, "");
			} catch (Exception e) {
				toSend = new UDPMessage("dpss_transferAccount_response", null, false, e.getMessage());
			}
			break;
		
			
		case "dpss_getPlayersStatus":
			try {
				String playersStatus = processor.getPlayersStatus(data.get("adminUsername"), data.get("adminPassword"), data.get("ipAddress"));
				toSend = new UDPMessage("dpss_getPlayersStatus_response", playersStatus, true, "");
			} catch (Exception e) {
				toSend = new UDPMessage("dpss_getPlayersStatus_response", null, false, e.getMessage());
			}
			break;
			
			default:
				logger.log("ERROR - Unknown request title: " + request.getMessage());
				return;
		}
		
		// Send back the response adding in the request ID
		toSend.setResponseId(request.getRequestId());
		
		logger.log("Leader processe finished computation, sending back response to frontend");
		
		if (!toSend.isSuccess())
			logger.log("Sending FAILURE ("+toSend.getMessage()+") to " + earliestRequestSender + ", reason: " + toSend.getErrorMessage());
		else
			logger.log("Sending SUCCESS ("+toSend.getMessage()+") to " + earliestRequestSender);
		UDPMessage.sendMessage(socket, toSend, earliestRequestSender );
		
	}

	/**
	 * Sets die so it can die in next loop
	 */
	public void die() {
		die = true;
	}

	/**
	 * Prints the current FIFO queue
	 */
	private void printFifoQueue(){
		
		synchronized (fifoQueue) {
			
			String print = "\n----------- FIFO QUEUE ELEMENTS ("+fifoQueue.size()+")------------";
			int count = 1;
			Iterator<UDPMessage> iter = fifoQueue.iterator();
			while(iter.hasNext()){
				print += "\n" + count + ")  " + iter.next().toString();
				count++;
			}
			print +=       "\n----------------------------------------------------------------\n";
			logger.out(print);
		}
	}

	/**
	 * Decrease the waiting for replica
	 */
	public synchronized void decreaseReplicaWaitingCounter() {
		waitingForReplicas.decrementAndGet();
		logger.log("Waiting for " + waitingForReplicas + " more replica(s) to respond");
	}

	/**
	 * Set my replicas in FifoQueueProcessor
	 * @param replicas
	 */
	public void setMyReplicas(HashSet<InetSocketAddress> replicas) {
		myReplicas = replicas;
	}
	
	
}
